<!DOCTYPE html>
<html><body>
  <div style="position:absolute;width:100%;z-index:1;font:14px Arial;color:white;text-align:center;">Press S for bounding sphere and B for bounding box</div>
  <script src="../lightgl.js"></script>
  <script src="cessna.js"></script>
  <script>

var time = 0;
var angleX = 20;
var angleY = 20;
var useBoundingSphere = true;
var gl = GL.create();
var mesh = GL.Mesh.load(cessna);
var cube = GL.Mesh.cube().computeWireframe();
var sphere = GL.Mesh.sphere({ detail: 3 }).computeWireframe();
var plane = GL.Mesh.plane({ normals: true, coords: true }).transform(GL.Matrix.scale(300, 300, 1));
var depthMap = new GL.Texture(1024, 1024, { format: gl.RGB });
var texturePlane = GL.Mesh.plane({ coords: true });
var boundingSphere = mesh.getBoundingSphere();
var boundingBox = mesh.getAABB();
var colorShader = new GL.Shader('\
  uniform vec3 center;\
  uniform vec3 radius;\
  void main() {\
    gl_Position = gl_ModelViewProjectionMatrix * vec4(gl_Vertex.xyz * radius + center, 1.0);\
  }\
', '\
  void main() {\
    gl_FragColor = vec4(0.0, 0.0, 1.0, 1.0);\
  }\
');
var depthShader = new GL.Shader('\
  varying vec4 pos;\
  void main() {\
    gl_Position = pos = gl_ModelViewProjectionMatrix * gl_Vertex;\
  }\
', '\
  varying vec4 pos;\
  void main() {\
    float depth = pos.z / pos.w;\
    gl_FragColor = vec4(depth * 0.5 + 0.5);\
  }\
');
var displayShader = new GL.Shader('\
  uniform mat4 shadowMapMatrix;\
  uniform vec3 light;\
  varying vec4 coord;\
  varying vec3 normal;\
  varying vec3 toLight;\
  void main() {\
    toLight = light - (gl_ModelViewMatrix * gl_Vertex).xyz;\
    normal = gl_NormalMatrix * gl_Normal;\
    gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;\
    coord = shadowMapMatrix * gl_Position;\
  }\
', '\
  uniform sampler2D depthMap;\
  varying vec4 coord;\
  varying vec3 normal;\
  varying vec3 toLight;\
  void main() {\
    float shadow = 0.0;\
    if (coord.w > 0.0) {\
      float depth = 0.0;\
      vec2 sample = coord.xy / coord.w * 0.5 + 0.5;\
      if (clamp(sample, 0.0, 1.0) == sample) {\
        float sampleDepth = texture2D(depthMap, sample).r;\
        depth = (sampleDepth == 1.0) ? 1.0e9 : sampleDepth;\
      }\
      if (depth > 0.0) {\
        float bias = -0.002;\
        shadow = clamp(300.0 * (bias + coord.z / coord.w * 0.5 + 0.5 - depth), 0.0, 1.0);\
      }\
    }\
    float ambient = 0.1;\
    float diffuse = max(0.0, dot(normalize(toLight), normalize(normal)));\
    gl_FragColor = vec4((normal * 0.5 + 0.5) * mix(ambient, 1.0, diffuse * (1.0 - shadow)), 1.0);\
  }\
');
var textureShader = new GL.Shader('\
  varying vec2 coord;\
  void main() {\
    coord = gl_TexCoord.xy;\
    gl_Position = vec4(coord * 2.0 - 1.0, 0.0, 1.0);\
  }\
', '\
  uniform sampler2D texture;\
  varying vec2 coord;\
  void main() {\
    gl_FragColor = texture2D(texture, coord);\
  }\
');

gl.onupdate = function(seconds) {
  time += seconds;
};

gl.onmousemove = function(e) {
  if (e.dragging) {
    angleY += e.deltaX;
    angleX = Math.max(-90, Math.min(90, angleX + e.deltaY));
  }
};

function cameraForBoundingSphere(light, boundingSphere) {
  var distance = boundingSphere.center.subtract(light).length();
  var angle = 180 - 2 * Math.acos(boundingSphere.radius / distance) * 180 / Math.PI;
  gl.matrixMode(gl.PROJECTION);
  gl.loadIdentity();
  gl.perspective(angle, 1, distance - boundingSphere.radius, distance + boundingSphere.radius);
  gl.matrixMode(gl.MODELVIEW);
  gl.loadIdentity();
  gl.lookAt(light.x, light.y, light.z, boundingSphere.center.x, boundingSphere.center.y, boundingSphere.center.z, 0, 1, 0);
}

function cameraForBoundingBox(light, boundingBox) {
  var center = boundingBox.min.add(boundingBox.max).divide(2);
  var axisZ = center.subtract(light).unit();
  var axisX = axisZ.cross(new GL.Vector(0, 1, 0)).unit();
  var axisY = axisX.cross(axisZ);
  var near = Number.MAX_VALUE;
  var far = -Number.MAX_VALUE;
  var slopeNegX = 0;
  var slopePosX = 0;
  var slopeNegY = 0;
  var slopePosY = 0;

  // Loop over all the points and find the maximum slope for each direction.
  // Incidentally, this algorithm works for convex hulls of any shape and will
  // return the optimal bounding frustum for every hull.
  for (var i = 0; i < 8; i++) {
    point = GL.Vector.lerp(boundingBox.min, boundingBox.max, new GL.Vector(!!(i & 1), !!(i & 2), !!(i & 4)));
    var toPoint = point.subtract(light);
    var dotZ = toPoint.dot(axisZ);
    var slopeX = toPoint.dot(axisX) / dotZ;
    var slopeY = toPoint.dot(axisY) / dotZ;
    slopeNegX = Math.min(slopeNegX, slopeX);
    slopeNegY = Math.min(slopeNegY, slopeY);
    slopePosX = Math.max(slopePosX, slopeX);
    slopePosY = Math.max(slopePosY, slopeY);
    near = Math.min(near, dotZ);
    far = Math.max(far, dotZ);
  }

  // Need to fit an oblique view frustum to get optimal bounds
  gl.matrixMode(gl.PROJECTION);
  gl.loadIdentity();
  gl.frustum(slopeNegX * near, slopePosX * near, slopeNegY * near, slopePosY * near, near, far);
  gl.matrixMode(gl.MODELVIEW);
  gl.loadIdentity();
  gl.lookAt(light.x, light.y, light.z, center.x, center.y, center.z, 0, 1, 0);
}

gl.ondraw = function() {
  // Move the light around
  var light = new GL.Vector(100 * Math.sin(time * 0.2), 25, 20 * Math.cos(time * 0.2));

  // Construct a camera looking from the light toward the object. The view
  // frustum is fit so it tightly encloses the bounding volume of the object
  // (sphere or box) to make best use of shadow map resolution. A frustum is
  // a pyramid shape with the apex chopped off.
  if (useBoundingSphere) {
    cameraForBoundingSphere(light, boundingSphere);
  } else {
    cameraForBoundingBox(light, boundingBox);
  }

  // Render the object viewed from the light using a shader that returns the
  // fragment depth.
  var shadowMapMatrix = gl.projectionMatrix.multiply(gl.modelviewMatrix);
  depthMap.unbind();
  depthMap.drawTo(function() {
    gl.clearColor(1, 1, 1, 1);
    gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
    depthShader.draw(mesh);
  });

  // Recover the corners of the view frustum
  var w = gl.canvas.width, h = gl.canvas.height;
  var eye = gl.modelviewMatrix.inverse().transformPoint(new GL.Vector());
  var corners = [
    gl.unProject(0, 0, 0),
    gl.unProject(w, 0, 0),
    gl.unProject(0, h, 0),
    gl.unProject(w, h, 0),
    gl.unProject(0, 0, 1),
    gl.unProject(w, 0, 1),
    gl.unProject(0, h, 1),
    gl.unProject(w, h, 1)
  ];

  // Set up the camera for the scene
  gl.clearColor(0, 0, 0, 1);
  gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
  gl.matrixMode(gl.PROJECTION);
  gl.loadIdentity();
  gl.perspective(45, gl.canvas.width / gl.canvas.height, 1, 1000);
  gl.matrixMode(gl.MODELVIEW);
  gl.loadIdentity();
  gl.translate(0, 0, -100);
  gl.rotate(angleX, 1, 0, 0);
  gl.rotate(angleY, 0, 1, 0);

  // Draw view frustum
  gl.pointSize(20);
  gl.begin(gl.LINES);
  for (var i = 0; i < 8; i++) {
    if (i < 4) {
      gl.color(1, 0, 0);
      gl.vertex(eye);
      gl.vertex(corners[i]);
    }
    gl.color(1, 1, 0);
    for (var j = 0; j < 3; j++) {
      gl.vertex(corners[i]);
      gl.vertex(corners[i ^ (1 << j)]);
    }
  }
  gl.end();

  // Draw the bounding volume
  if (useBoundingSphere) {
    colorShader.uniforms({
      center: boundingSphere.center,
      radius: new GL.Vector(boundingSphere.radius, boundingSphere.radius, boundingSphere.radius)
    }).draw(sphere, gl.LINES);
  } else {
    colorShader.uniforms({
      center: boundingBox.min.add(boundingBox.max).divide(2),
      radius: boundingBox.max.subtract(boundingBox.min).divide(2)
    }).draw(cube, gl.LINES);
  }

  // Draw mesh
  depthMap.bind();
  displayShader.uniforms({
    shadowMapMatrix: shadowMapMatrix.multiply(gl.projectionMatrix.multiply(gl.modelviewMatrix).inverse()),
    light: gl.modelviewMatrix.transformPoint(light)
  }).draw(mesh);

  // Draw plane
  gl.pushMatrix();
  gl.rotate(-90, 1, 0, 0);
  displayShader.draw(plane);
  gl.popMatrix();

  // Draw depth map overlay
  gl.viewport(10, 10, 10 + 256, 10 + 256);
  textureShader.draw(texturePlane);
  gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
};

document.onkeydown = function(e) {
  if (e.which == 'S'.charCodeAt(0)) {
    useBoundingSphere = true;
  } else if (e.which == 'B'.charCodeAt(0)) {
    useBoundingSphere = false;
  }
};

gl.fullscreen();
gl.animate();
gl.enable(gl.DEPTH_TEST);

  </script>
</body></html>
